using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using DslCrossModelFramework.VSPackage;
using Microsoft.VisualStudio.Modeling;

namespace DslCrossModelFramework {
    /// <summary>
    /// Implementation of <see cref="IModelFileInfo"/> which will provide extra meta data about the <see cref="ModelFile"/>
    /// </summary>
    public class ModelFileInfo : IModelFileInfo {
        private readonly ModelDirectory modelDirectory;
        private readonly Dictionary<string, Guid> modelReferences = new Dictionary<string, Guid>();

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelFileInfo"/> class.
        /// </summary>
        /// <param name="modelFile">The model file.</param>
        /// <param name="dslPackageInfo">The DSL package info.</param>
        /// <param name="modelDirectory">The model directory.</param>
        /// <param name="store">The store.</param>
        public ModelFileInfo(IModelFile modelFile, DslToolsPackageInfo dslPackageInfo, ModelDirectory modelDirectory, Store store){
            this.modelFile = modelFile;
            this.dslPackageInfo = dslPackageInfo;
            this.modelDirectory = modelDirectory;
            this.store = store;
            isLoaded = false;
            Loading += ModelFileInfo_Loading;
            Loaded += ModelFileInfo_Loaded;
        }

        private readonly List<ModelElement> addedElementQueue = new List<ModelElement>();

        void ModelFileInfo_Loading(object sender, ModelFileEventArgs e) {
            addedElementQueue.Clear();
        }

        void ModelFileInfo_Loaded(object sender, ModelFileEventArgs e) {
            if(!IsDynamic) {
                MakeElementReferenceDictionary();
                addedElementQueue.Clear();
            }
        }

        /// <summary>
        /// Gets the model root.
        /// </summary>
        /// <value>The model root.</value>
        public ModelElement ModelRoot {
            get {
                if(!isLoaded) {
                    Load();
                }
                return modelRoot;
            }
        }
        private ModelElement modelRoot;

        /// <summary>
        /// Gets the model file.
        /// </summary>
        /// <value>The model file.</value>
        public IModelFile ModelFile {
            get { return modelFile; }
        }
        private readonly IModelFile modelFile;
        
        public bool IsLoaded {
            get {
                return isLoaded;
            }
        }
        private bool isLoaded;

        /// <summary>
        /// Gets the DSL package information.
        /// </summary>
        /// <value>The DSL package information.</value>
        public DslToolsPackageInfo DslPackageInfo {
            get { return dslPackageInfo; }
        }
        private readonly DslToolsPackageInfo dslPackageInfo;

        /// <summary>
        /// Loads this instance.
        /// </summary>
        /// <returns>The root element of the loaded model</returns>
        public ModelElement Load() {
            if (!isLoaded) {
                if(Loading != null){Loading(this, new ModelFileEventArgs(this));}
                if(ElementAddedEventHandler == null) {
                    ElementAddedEventHandler = new EventHandler<ElementAddedEventArgs>(OnModelElementAdded);
                }
                store.EventManagerDirectory.ElementAdded.Add(
                    ElementAddedEventHandler);

                try {
                    ModelElement mel = modelDirectory.LoadModelFile(modelFile, dslPackageInfo, store);

                    

                    if (mel != null) {
                        isLoaded = true;
                        modelRoot = mel;
                    }
                    if (Loaded != null) { Loaded(this, new ModelFileEventArgs(this)); }  

                }
                catch(Exception) {
                    //TODO: Throw exception as load failed
                    throw;

                }
                finally {
                    store.EventManagerDirectory.ElementAdded.Remove(ElementAddedEventHandler);
                }
            }
            return modelRoot;            
        }

        public EventHandler<ElementAddedEventArgs> ElementAddedEventHandler;

        /// <summary>
        /// Loads this instance using the specified model file helper.
        /// </summary>
        /// <param name="modelFileHelper">The model file helper.</param>
        /// <returns>The root element of the loaded model</returns>
        public ModelElement Load(IModelFileHelper modelFileHelper) {
            if (!isLoaded) {
                if (Loading != null) { Loading(this, new ModelFileEventArgs(this)); }
                if (ElementAddedEventHandler == null) {
                    ElementAddedEventHandler = new EventHandler<ElementAddedEventArgs>(OnModelElementAdded);
                }
                store.EventManagerDirectory.ElementAdded.Add(ElementAddedEventHandler);

                ModelElement mel =
                    modelDirectory.LoadModelFile(modelFile, modelFileHelper, dslPackageInfo, store);

                store.EventManagerDirectory.ElementAdded.Remove(ElementAddedEventHandler);

                if (mel != null) {
                    isLoaded = true;
                    modelRoot = mel;
                }
                if (Loaded != null) { Loaded(this, new ModelFileEventArgs(this)); }
            }
            return modelRoot;
        }

        private void MakeElementReferenceDictionary() {
            foreach(ModelElement mel in addedElementQueue) {
                if(modelDirectory.CanReferenceModelElement(mel.GetDomainClass())) {
                    modelReferences.Add(modelDirectory.GetModelElementReference(mel), mel.Id);
                }
            }
        }


        private void OnModelElementAdded(object sender, ElementAddedEventArgs e) {
            addedElementQueue.Add(e.ModelElement);
        }

        /// <summary>
        /// Gets a value indicating whether this instance is dynamic.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is dynamic; otherwise, <c>false</c>.
        /// </value>
        public bool IsDynamic {
            get {
                return isDynamic;
            }
        }

        /// <summary>
        /// Unloads this instance.
        /// </summary>
        public void Unload() {
            if(!isDynamic) {
                if(Unloading != null) {this.Unloading(this, new ModelFileEventArgs(this));}
                using(Transaction tx = modelRoot.Store.TransactionManager.BeginTransaction("Removing model from shared store")) {
                    modelRoot.Delete();
                    tx.Commit();
                }
                modelReferences.Clear();
                modelRoot = null;
                if (Unloaded != null) { this.Unloaded(this, new ModelFileEventArgs(this)); }
            }
        }

        /// <summary>
        /// Determines whether this instance contains the specified model element.
        /// </summary>
        /// <param name="mel">The mel.</param>
        /// <returns>
        /// 	<c>true</c> if this instance contains the specified model element; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(ModelElement mel) {
            if(!isDynamic) {
                // Search the element directory
                string melRef = modelDirectory.GetModelElementReference(mel);
                if(modelReferences.ContainsKey(melRef)) {
                    return true;
                }
            }
            else {
                if(Store.Id == mel.Store.Id) {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Gets the element by its serialized string reference.
        /// </summary>
        /// <param name="reference">The reference.</param>
        /// <returns></returns>
        public ModelElement GetElementByReference(string reference) {
            if(!isDynamic) {
                if (modelReferences.ContainsKey(reference)) {
                    return store.ElementDirectory.GetElement(modelReferences[reference]);
                }
            }
            else {
                foreach(ModelElement mel in store.ElementDirectory.AllElements) {
                    if(modelDirectory.GetModelElementReference(mel) == reference) {
                        return mel;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Gets all the elements in the model file.
        /// </summary>
        /// <returns></returns>
        public ReadOnlyCollection<ModelElement> AllElements() {
            if(!isDynamic) {
                if (!IsLoaded) {
                    Load();
                }
                // Get all elements in the element directory
                return new ReadOnlyCollection<ModelElement>(store.ElementDirectory.GetElements(modelReferences.Values));
            }
            else {
                // Get all elements in the dynamic store
                return new ReadOnlyCollection<ModelElement>(new List<ModelElement>(store.ElementDirectory.AllElements));
            }
        }

        /// <summary>
        /// Makes the model file dynamic based on a live store from the IDE.
        /// </summary>
        /// <param name="dynamicStore">The dynamic store.</param>
        public void MakeDynamic(Store dynamicStore) {
            if(this.isLoaded) {
                if (Unloading != null) { Unloading(this, new ModelFileEventArgs(this)); }
                Unload();
                if (Unloaded != null) { Unloaded(this, new ModelFileEventArgs(this)); }
            }
            
            store = dynamicStore;
            isDynamic = true;
            modelReferences.Clear();
            isLoaded = true;
            modelRoot = store.ElementDirectory.FindElements((Guid)Type.GetType(DslPackageInfo.ModelElementRootType).InvokeMember("DomainClassId", BindingFlags.Public | BindingFlags.Static |BindingFlags.GetField, null, null, new object[]{}))[0];
            if (Loading != null) { Loading(this, new ModelFileEventArgs(this)); }
            if(Loaded!= null){Loaded(this, new ModelFileEventArgs(this));}
        }

        private bool isDynamic;

        /// <summary>
        /// Makes the model file shared so that it will share the same store as other non-dynamic model files.
        /// </summary>
        /// <param name="sharedStore">The shared store.</param>
        public void MakeShared(Store sharedStore) {
            if (this.isLoaded) {
                if (Unloading != null) { Unloading(this, new ModelFileEventArgs(this)); }
                if (Unloaded != null) { Unloaded(this, new ModelFileEventArgs(this)); }
            }
            
            store = sharedStore;
            isDynamic = false;
            isLoaded = false;
            modelReferences.Clear();
            
            modelRoot = null;

            if (Loading != null) { Loading(this, new ModelFileEventArgs(this)); }
            if (Loaded != null) { Loaded(this, new ModelFileEventArgs(this)); }
        }

        /// <summary>
        /// Occurs when the model file is about to begin loading.
        /// </summary>
        public event EventHandler<ModelFileEventArgs> Loading;

        /// <summary>
        /// Occurs when the model file is loaded.
        /// </summary>
        public event EventHandler<ModelFileEventArgs> Loaded;

        /// <summary>
        /// Occurs when the model file is about to begin unloading.
        /// </summary>
        public event EventHandler<ModelFileEventArgs> Unloading;

        /// <summary>
        /// Occurs when the model file is unloaded.
        /// </summary>
        public event EventHandler<ModelFileEventArgs> Unloaded;

        /// <summary>
        /// Gets the store that is used for the model elements in the model file.
        /// </summary>
        /// <value>The store.</value>
        public Store Store {
            get { return store; }
        }

        private Store store;
    }
}